package com.log.aspect;

import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.extra.servlet.ServletUtil;
import com.alibaba.fastjson.JSONObject;
import com.log.annotation.SysLog;
import com.log.event.SysLogEvent;
import com.log.filter.StringFilter;
import com.log.pojo.OptLog;
import com.log.utils.ContextUtil;
import com.log.utils.SpringUtils;
import com.log.utils.LogUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.MDC;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;

/**
 * @Author zhang
 * @create 2023/8/5 17:29
 */
@Slf4j
@Aspect
@Component
public class SysLogAspect {
    // 最大长度
    public static final int MAX_LENGTH = 65535;
    // 线程存储日志
    private static final ThreadLocal<OptLog> THREAD_LOCAL = new ThreadLocal<>();
    // 请求格式类型
    private static final String FORM_DATA_CONTENT_TYPE = "multipart/form-data";
    /*
     * 日志链路追踪id日志标志
     */
    String LOG_TRACE_ID = "trace";
    /*
     * 日志链路追踪id信息头
     */
    String TRACE_ID_HEADER = "x-trace-header";

    /***
     * 定义controller切入点拦截规则：拦截标记SysLog注解和指定包下的方法
     * 2个表达式加起来才能拦截所有Controller 或者继承了BaseController的方法
     * execution(public * com.*.*.controller.*.*(..)) 解释：
     * 第一个* 任意返回类型
     * 第二个* com.*.*.controller包下的所有类
     * 第三个* 类下的所有方法
     * ()中间的.. 任意参数
     * \@annotation(com.log.annotation.SysLog) 解释：
     * 标记了@SysLog 注解的方法
     */
    @Pointcut("execution(public * com.*.*.controller.*.*(..)) || @annotation(com.log.annotation.SysLog)")
    public void sysLogAspect() {
    }

    /**
     * 返回通知
     *
     * @param result    返回值
     * @param joinPoint 端点
     */
    @AfterReturning(returning = "result", pointcut = "sysLogAspect()")
    public void doAfterReturning(JoinPoint joinPoint, Object result) {
//        log.info("切面之后获取数据" + result);
        tryCatch(p -> {
            SysLog sysLog = LogUtil.getTargetAnnotation(joinPoint);
            if (checkLogRecord(joinPoint, sysLog)) return;
            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            if (attributes == null) return;
            HttpServletRequest request = attributes.getRequest();
            String url = request.getRequestURI();
            if (StringUtils.isEmpty(url)) return;
            url = url.toLowerCase();
            if (StringFilter.isStringExist(url)) return;
            // 获取方法名
            String methodName = null;
            if (joinPoint != null && joinPoint.getSignature() != null) {
                // 获取方法名
                methodName = joinPoint.getSignature().getName();
            }
            if (StringUtils.isEmpty(methodName)) return;
            methodName = methodName.toLowerCase();
            // 如果方法中是获取则返回
            if (StringFilter.isStringExist(methodName)) return;
            String json = JSONObject.toJSONString(result);
            if (StringUtils.isEmpty(json)) return;
            Map<String, Object> map = JSONObject.parseObject(json);
            if (CollectionUtils.isEmpty(map)) return;
            Integer status = 0;
            String msg = "失败";
            if (map.containsKey("status")) {
                status = (Integer) map.get("status");
            }
            if (map.containsKey("msg")) {
                msg = (String) map.get("msg");
            }
            OptLog sysLogDTO = get();
            sysLogDTO.setMultiTenantType(sysLog.multiTenantType());
            sysLogDTO.setType(sysLog.type());
            sysLogDTO.setTypeDesc(sysLog.value());
            sysLogDTO.setStatus(status);
            sysLogDTO.setMsg(msg);
            sysLogDTO.setStartTime(LocalDateTime.now());
            sysLogDTO.setActionMethod(methodName);
            sysLogDTO.setDescription(LogUtil.getDescribe(sysLog));
            sysLogDTO.setResult(json);
            sysLogDTO.setRequestUri(url);
            sysLogDTO.setHttpMethod(request.getMethod());
            publishEvent(sysLogDTO);
        });
    }

    /**
     * 异常通知
     *
     * @param joinPoint 端点
     * @param e         异常
     */
    @AfterThrowing(pointcut = "sysLogAspect()", throwing = "e")
    public void doAfterThrowable(JoinPoint joinPoint, Throwable e) {
        tryCatch((val) -> {
            log.info("切面异常数据" + val);
            SysLog sysLog = LogUtil.getTargetAnnotation(joinPoint);
            if (checkLogRecord(joinPoint, sysLog)) {
                return;
            }
            OptLog optLogDTO = get();
            // 数据库类型
            optLogDTO.setMultiTenantType(sysLog.multiTenantType());
            // 用户信息及租户
            optLogDTO.setTenantCode(ContextUtil.getTenant());
            optLogDTO.setCreatedBy(ContextUtil.getAccountId());
            optLogDTO.setUserName(ContextUtil.getUsername());
            // 异常类型为4
            optLogDTO.setType(4);
            optLogDTO.setTypeDesc("异常");
            // 遇到错误时，请求参数若为空，则记录
            if (!sysLog.request() && sysLog.requestByError() && StrUtil.isEmpty(optLogDTO.getParams())) {
                Object[] args = joinPoint.getArgs();
                HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
                String strArgs = getArgs(args, request);
                optLogDTO.setParams(getText(strArgs));
            }
            // 异常对象
            optLogDTO.setExDetail(ExceptionUtil.stacktraceToString(e, MAX_LENGTH));
            publishEvent(optLogDTO);
        });
    }

    /**
     * 执行方法之前
     *
     * @param joinPoint 端点
     */
    @Before(value = "sysLogAspect()")
    public void doBefore(JoinPoint joinPoint) {
//        log.info("切面开始");
        tryCatch(val -> {
            SysLog sysLog = LogUtil.getTargetAnnotation(joinPoint);
            if (checkLogRecord(joinPoint, sysLog)) return;
            OptLog optLogDTO = buildOptLogDTO(joinPoint, sysLog);
            THREAD_LOCAL.set(optLogDTO);
        });
    }

    @NonNull
    private OptLog buildOptLogDTO(JoinPoint joinPoint, SysLog sysLog) {
        // 开始时间
        OptLog optLogDTO = get();
        // 类名
        optLogDTO.setClassPath(joinPoint.getTarget().getClass().getName());
        //获取执行的方法名
        optLogDTO.setActionMethod(joinPoint.getSignature().getName());
        // 描述
//        setDescription(joinPoint, sysLog, optLogDTO);
        HttpServletRequest request = setParams(joinPoint, sysLog, optLogDTO);
        optLogDTO.setRequestIp(ServletUtil.getClientIP(request));
        optLogDTO.setRequestUri(URLUtil.getPath(request.getRequestURI()));
        optLogDTO.setHttpMethod(request.getMethod());
        optLogDTO.setUa(StrUtil.sub(request.getHeader("user-agent"), 0, 500));

        optLogDTO.setTrace(MDC.get(LOG_TRACE_ID));
        if (StrUtil.isEmpty(optLogDTO.getTrace())) {
            optLogDTO.setTrace(request.getHeader(TRACE_ID_HEADER));
        }
        optLogDTO.setStartTime(LocalDateTime.now());
        // 用户信息及租户
        optLogDTO.setTenantCode(ContextUtil.getTenant());
        optLogDTO.setCreatedBy(ContextUtil.getAccountId());
        optLogDTO.setUserName(ContextUtil.getUsername());
        return optLogDTO;
    }

    @NonNull
    private HttpServletRequest setParams(JoinPoint joinPoint, SysLog sysLog, OptLog optLogDTO) {
        // 请求体
        HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes(), "只能在Spring Web环境使用@SysLog记录日志")).getRequest();
        // 参数
        Object[] args = joinPoint.getArgs();
        if (args == null || args.length == 0) return request;
        if (sysLog.request()) {
            String strArgs = getArgs(args, request);
            optLogDTO.setParams(getText(strArgs));
        }
        return request;
    }


    // 获取日志象
    private OptLog get() {
        OptLog sysLog = THREAD_LOCAL.get();
        if (sysLog == null) {
            return new OptLog();
        }
        return sysLog;
    }

    private void tryCatch(Consumer<String> consumer) {
        try {
            consumer.accept("");
        } catch (Exception e) {
            log.warn("记录操作日志异常", e);
            THREAD_LOCAL.remove();
        }
    }

    // 监听事件
    private void publishEvent(OptLog sysLog) {
        SpringUtils.publishEvent(new SysLogEvent(sysLog));
        THREAD_LOCAL.remove();
    }

    /**
     * 监测是否需要记录日志
     *
     * @param joinPoint 端点
     * @param sysLog    操作日志
     * @return true 表示不需要记录日志
     */
    private boolean checkLogRecord(JoinPoint joinPoint, SysLog sysLog) {
        if (sysLog == null || !sysLog.enabled()) {
            return true;
        }
        // 读取目标类上的注解
        SysLog targetClass = joinPoint.getTarget().getClass().getAnnotation(SysLog.class);
        // 加上 sysLog == null 会导致父类上的方法永远需要记录日志
        return targetClass != null && !targetClass.enabled();
    }

    /**
     * 截取指定长度的字符串
     *
     * @param val 参数
     * @return 截取文本
     */
    private String getText(String val) {
        return StrUtil.sub(val, 0, 65535);
    }

    private String getArgs(Object[] args, HttpServletRequest request) {
        String strArgs = "";
        Object[] params = Arrays.stream(args).filter(item -> !(item instanceof ServletRequest || item instanceof ServletResponse)).toArray();
        if (params.length == 0) return strArgs;
        try {
            if (!request.getContentType().contains(FORM_DATA_CONTENT_TYPE)) {
                strArgs = JSONObject.toJSONString(params);
            }
        } catch (Exception e) {
            try {
                strArgs = Arrays.toString(params);
            } catch (Exception ex) {
                log.warn("解析参数异常", ex);
            }
        }
        return strArgs;
    }

}
